﻿using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;

public enum GLogType
{
    All,
    Debug,
    Info,
    Warning,
    Exception,
    Error,
    Assert,
    OFF,
}
public sealed class LogAgent
{
    private static string logDirectoryPath = Application.persistentDataPath + "/log/";
    private static IGameLogCallBack logCallBack;
    private static bool bEnableDebugLog = false;
    private static Thread mainThread;

    /// <summary>
    /// 本地保存的最大日志文件数量
    /// </summary>
    private const int MaxLogFileLen = 50;
    
    /// <summary>
    /// 单次游戏允许写入日志文件的字符串最长长度
    /// </summary>
    #if UNITY_EDITOR||UNITY_STANDALONE||DEBUG||DEVELOPMENT_BUILD
    private const int MaxLogStringLen = 500000000;
    #else
    private const int MaxLogStringLen = 100000000;
    #endif
    
    /// <summary>
    /// 打印到本地文件 间隔时间
    /// </summary>
    private const int OutFileWaitTime = 3000;
    private static System.Threading.Thread logThread;
    /// <summary>
    /// 日志进程锁
    /// </summary>
    private static System.Object msgLock = new System.Object();
    private static System.Object pauseLock = new System.Object();
    private static bool bPause = false;
    private static StringBuilder fileLogStringBuilder = new StringBuilder();
    //堆栈日志的字典，输出过堆栈的日志，如果再次出现则不输出堆栈了，提示前面有堆栈，key是堆栈字符串，value是该堆栈第一次出现的时间和帧号
    private static Dictionary<string,string> stackHistoryDict =new Dictionary<string, string>();
    private static string logPath = null;
    private static StreamWriter streamWriter;
    private static long alreadyWriteLength = 0;


    static LogAgent()
    {
        Application.logMessageReceivedThreaded +=OnUnityLog;//接收所有线程的日志，注意不能随意调用UnityAPI，因为可能是非主线程触发
    }
    
    public static void OnApplicationQuit()
    {
        CloseLogFile();
        if (logCallBack != null)
        {
            logCallBack.OnDestroy();
            logCallBack = null;
        }

        if (logThread != null)
        {
            logThread.Abort();
            logThread = null;
        }
    }

    public static bool SetPause(bool bPause)
    {
        log(GLogType.Info, "LogAgent.SetPause()，" + bPause);
        lock (pauseLock)
        {
            LogAgent.bPause = bPause;
        }
        if(bPause)
        {
            CloseLogFile();
        }
        return true;
    }

    public static string GetLogPath()
    {
        return logPath;
    }
    
    public static string GetLogDirectoryPath()
    {
        return logDirectoryPath;
    }
    
    private static bool CloseLogFile()
    {
        WriteToLogFile();
        if (streamWriter != null)
        {
            streamWriter.Close();
            streamWriter = null;
        }
        return true;
    }

    #region 本地日志写入
    /// <summary>
    /// 日志数据写入到本地文件中
    /// </summary>
    private static void WriteToLogFile()
    {
        try
        {
            string msg = null;
            lock (msgLock)
            {
                if (fileLogStringBuilder == null || fileLogStringBuilder.Length == 0) return;
                msg = fileLogStringBuilder.ToString();
                fileLogStringBuilder.Length = 0;
            }

            if (!string.IsNullOrEmpty(msg))
            {
                if (streamWriter == null && !string.IsNullOrEmpty(logPath))
                {
                    streamWriter = new StreamWriter(logPath, true);
                }
                streamWriter.Write(msg);
                streamWriter.Flush();
            }
        }
        catch (Exception e)
        {
            Debug.LogException(e);
        }
    }
    
    private static void WriteFileThreadFunc()
    {
        while (true)
        {
            try
            {
                bool bCanWrite = true;
                lock (pauseLock)
                {
                    bCanWrite = !bPause;
                }
                if(bCanWrite)
                {
                    WriteToLogFile();
                }
                Thread.Sleep(OutFileWaitTime);
            }
            catch (Exception e)
            {
                //主动打断的不报错
                if (!(e is ThreadInterruptedException))
                {
                    log(GLogType.Exception,e.Message);
                }
            }
        }
    }
    #endregion

    public static void Init(IGameLogCallBack gameLogCB)
    {
        mainThread = Thread.CurrentThread;
        logCallBack = gameLogCB;
        if (logCallBack != null) 
            logCallBack.OnInit();
        InitLogFile();
        if (logThread == null)
        {
            logThread = new System.Threading.Thread(WriteFileThreadFunc);
            logThread.Priority = System.Threading.ThreadPriority.Lowest;
            logThread.Name = "LogThread";
            logThread.Start();
        }
    } private static void InitLogFile()
    {
        try
        {
            if (!Directory.Exists(logDirectoryPath))
            {
                Debug.LogFormat("CreateDirectory logDirectory:{0}",logDirectoryPath);
                Directory.CreateDirectory(logDirectoryPath);
            }
            else
            {//清理历史日志
                DirectoryInfo di = new DirectoryInfo(logDirectoryPath);
                var fileInfos = di.GetFiles("*.log", SearchOption.TopDirectoryOnly);
                if (fileInfos.Length > MaxLogFileLen)
                {
                    Array.Sort(fileInfos, (FileInfo f1, FileInfo f2) => { return f1.CreationTime.CompareTo(f2.CreationTime); });//按文件创建时间排正序
                    for (int i = 0; i < fileInfos.Length-MaxLogFileLen; i++)
                    {
                        fileInfos[i].Delete();
                    }
                }
            }
            logPath = logDirectoryPath + DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log";
            Debug.LogFormat("logPath:{0}",logPath);
        }
        catch
        {
            Debug.LogError("[Error]游戏日志系统 因为游戏双开造成了文件操作异常所以不能操作文件");
        }
    }

    /// <summary>
    /// 在日志文件名称中加入额外信息
    /// </summary>
    /// <param name="appendInfo"></param>
    public static void AddLogPathInfo(string appendInfo)
    {
        if (string.IsNullOrEmpty(logPath)||string.IsNullOrEmpty(appendInfo))
        {
            return;
        }

        if (logPath.Contains(appendInfo))
        {    //返回登录会再次触发添加信息，只有选择的服务器或角色不同时才往后添加
            return;
        }
        
        try
        {
            string fileName = Path.GetFileNameWithoutExtension(logPath);
            string oldLogPath = logPath;
            string newLogPath = logDirectoryPath +fileName+appendInfo + ".log";
            Debug.LogFormat("AddLogPathInfo,new logPath:{0}",newLogPath);
            SetPause(true);
            if (File.Exists(oldLogPath))
            {
                File.Move(oldLogPath,newLogPath);
            }
            logPath = newLogPath;
            SetPause(false);
        }
        catch (Exception e)
        {
            Debug.LogError("[Error]游戏日志系统 AddLogPathInfo异常:"+e.Message);
        }
    }

    private static GLogType GetGType(LogType lt)
    {
        switch (lt)
        {
            case LogType.Error:
                return GLogType.Error;
            case LogType.Assert:
                return GLogType.Assert;
            case LogType.Warning:
                return GLogType.Warning;
            case LogType.Log:
                return GLogType.Info;
            case LogType.Exception:
                return GLogType.Exception; 
            default:
                return GLogType.Info;
        } 
    }
    
    private static string GetTypeString(LogType lt)
    {
        switch (lt)
        {
            case LogType.Error:
                return "Error";
            case LogType.Assert:
                return "Assert";;
            case LogType.Warning:
                return "Warning";;
            case LogType.Log:
                return "Log";;
            case LogType.Exception:
                return "Exception";; 
            default:
                return "Log";;
        } 
    }
    
    private static StringBuilder tempStringBuilder=new StringBuilder();

    public static bool IsMainThread()
    {
        return mainThread == Thread.CurrentThread;
    }
     private static void OnUnityLog(string msg, string stacktrace, LogType type)
    {
        try
        {
            if (alreadyWriteLength > MaxLogStringLen)
            {//记录的日志长度大于一定值之后不再记录(一般都是出现了update异常，一直刷日志导致的)
                string tips = "Log file size is too larger,stop write log file";
                logCallBack?.OnCallLogToServer(GLogType.Error, tips); 
                lock (msgLock)
                {
                    fileLogStringBuilder.AppendLine(tips);
                }
                SetPause(true);
                Application.logMessageReceivedThreaded -= OnUnityLog;
                if (logThread != null)
                {
                    logThread.Abort();
                    logThread = null;
                }
                stackHistoryDict.Clear();
                Debug.Log(tips);
                return;
            }
            bool bMyStack = false;
            if (string.IsNullOrEmpty(stacktrace))
            {
                stacktrace = new System.Diagnostics.StackTrace().ToString();
                bMyStack = true;
            }
            else
            {
                //Unity日志回调本身附带的堆栈格式没有缩进，需要处理一下
                stacktrace = stacktrace.Replace("\n", "\n    ");
            }

            lock (msgLock)
            {
                //支持处理多线程日志，
                tempStringBuilder.Append(DateTime.Now.ToString("[HH:mm:ss][")).Append(GetTypeString(type)).Append("]");

                if (!msg.StartsWith("["))
                {
                    //直接调用UnityEngine.Debug.Log接口时没有帧号，这里补充上帧号,如果可以改引擎源码更好，那样编辑器里的日志也能加上帧号
                    if (IsMainThread()) //UnityEngine.Time.frameCount只能在主线程中执行，日志接口可以在任何线程中调用
                    {
                        try
                        {
                            //TODO 在序列化期间/构造期间底层可能有Log，到这里调用UnityEngine.Time.frameCount会异常？如果比较多，可能还是要删掉这里的代码
                            tempStringBuilder.AppendFormat("[{0}]", UnityEngine.Time.frameCount);
                        }
                        catch (Exception)
                        {
                        }
                    }
                }

                tempStringBuilder.Append(msg);
                string flagStr = tempStringBuilder.ToString();
                if (type == LogType.Exception || type == LogType.Assert || type == LogType.Error)
                {
                    string stackHistory = null;
                    if (stackHistoryDict.TryGetValue(stacktrace, out stackHistory))
                    {
                        tempStringBuilder.Append(stackHistory);
                    }
                    else
                    {
                        tempStringBuilder.AppendLine();
                        if (!bMyStack)
                        {
                            tempStringBuilder.Append("    ");
                        }

                        tempStringBuilder.Append(stacktrace);
                        if (stackHistoryDict.Count < 1000)
                        {
                            stackHistoryDict[stacktrace] = "   ...Stacktrace already exists at :" + flagStr;
                        }
                        else
                        {
                            tempStringBuilder.Append("\nstackHistoryDict数量过大，不再进行过滤缓存");
                        }
                    }
                }

                tempStringBuilder.AppendLine();
                alreadyWriteLength += tempStringBuilder.Length;
                String tempMsg = tempStringBuilder.ToString();
                tempStringBuilder.Length = 0;

                fileLogStringBuilder.Append(tempMsg);
                
                GLogType gtype = GetGType(type);
                logCallBack?.OnCallLogToServer(gtype, tempMsg); 
            }

        }
        catch (Exception ex)
        {
            UnityEngine.Debug.LogException(ex);
        }
    }

    public static void log(GLogType type, string msg)
    {
        try
        {
            if (string.IsNullOrEmpty(msg)) return;
            switch (type)
            {
                case GLogType.Debug:
                    UnityEngine.Debug.Log(msg);
                    break;
                case GLogType.Info:
                    UnityEngine.Debug.Log(msg);
                    break;
                case GLogType.Warning:
                    UnityEngine.Debug.LogWarning(msg);
                    break;
                case GLogType.Exception:
                case GLogType.Error:
                    UnityEngine.Debug.LogError(msg);
                    break;
                case GLogType.Assert:
                    UnityEngine.Debug.LogAssertion(msg);
                    break;
            }

        }
        catch (Exception e)
        {
            UnityEngine.Debug.LogError("[Exception] Exception occurred: " + e.GetType().Name + ", Message: " + e.Message);
        }
    }
}


    